Hanye官网
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

[id].vue 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  1. <template>
  2. <div>
  3. <div class="w-full h-[55px] sm:h-[72px]"></div>
  4. <ErrorBoundary :error="error">
  5. <div v-if="isLoading" class="flex justify-center py-12">
  6. <!-- 加载中 -->
  7. <div
  8. class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent"
  9. ></div>
  10. </div>
  11. <div v-else>
  12. <!-- 面包屑导航 -->
  13. <div class="max-w-full mb-6 xl:px-2 lg:px-2 md:px-4 px-4 mt-6">
  14. <div class="max-w-screen-2xl mx-auto">
  15. <nuxt-link
  16. to="/"
  17. class="justify-start text-white/60 text-base font-normal"
  18. >ホーム</nuxt-link
  19. >
  20. <span class="text-white/60 text-base font-normal px-2"> / </span>
  21. <nuxt-link
  22. to="/products"
  23. class="text-white/60 text-base font-normal"
  24. >製品一覧</nuxt-link
  25. >
  26. <span class="text-white/60 text-base font-normal px-2"> / </span>
  27. <nuxt-link
  28. v-if="product?.category"
  29. :to="`/products?category=${encodeURIComponent(product.category)}`"
  30. class="text-white/60 text-base font-normal"
  31. >{{ product.category }}</nuxt-link
  32. >
  33. <span class="text-white/60 text-base font-normal px-2"> / </span>
  34. <span class="text-white text-base font-normal">{{
  35. product?.title || product?.name
  36. }}</span>
  37. </div>
  38. </div>
  39. <!-- 产品详情内容 -->
  40. <div
  41. v-if="product"
  42. class="max-w-full mb-12 md:mb-20 lg:mb-32 xl:px-2 lg:px-2 md:px-4 px-4"
  43. >
  44. <div class="max-w-screen-2xl mx-auto">
  45. <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16">
  46. <!-- 左侧产品图片 -->
  47. <div class="flex flex-col gap-6 lg:sticky lg:top-24 self-start">
  48. <!-- 主图展示 -->
  49. <div
  50. class="bg-zinc-900 rounded-lg p-8 relative overflow-hidden group aspect-square"
  51. >
  52. <!-- 加载状态 -->
  53. <div
  54. v-if="isImageLoading"
  55. class="absolute inset-0 flex items-center justify-center bg-zinc-800/50 z-10"
  56. >
  57. <div
  58. class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent"
  59. ></div>
  60. </div>
  61. <!-- 主图容器 -->
  62. <div class="relative w-full h-full">
  63. <!-- 当前图片 -->
  64. <img
  65. :src="currentImage"
  66. :alt="product.name"
  67. class="absolute inset-0 w-full h-full object-contain rounded-lg transition-all duration-500"
  68. :class="{
  69. 'opacity-0': isImageLoading,
  70. 'opacity-100': !isImageLoading,
  71. }"
  72. @load="handleImageLoad"
  73. @error="handleImageError"
  74. />
  75. <!-- 预加载图片 -->
  76. <img
  77. v-if="preloadImage"
  78. :src="preloadImage"
  79. class="absolute inset-0 w-full h-full object-contain rounded-lg opacity-0"
  80. @load="handlePreloadComplete"
  81. />
  82. </div>
  83. <!-- 错误提示 -->
  84. <div
  85. v-if="imageError"
  86. class="absolute inset-0 flex items-center justify-center bg-red-900/50 z-20"
  87. >
  88. <div class="flex flex-col items-center gap-2">
  89. <span class="text-white"
  90. >画像の読み込みに失敗しました</span
  91. >
  92. <button
  93. @click.stop="retryLoadImage"
  94. class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors duration-300"
  95. >
  96. 再試行
  97. </button>
  98. </div>
  99. </div>
  100. </div>
  101. <!-- 缩略图列表 -->
  102. <div class="flex gap-4 overflow-x-auto pb-2 scrollbar-hide">
  103. <div
  104. v-for="(image, index) in [
  105. product.image,
  106. ...(product.gallery || []),
  107. ]"
  108. :key="index"
  109. @click="changeImage(image)"
  110. class="flex-shrink-0 w-20 h-20 cursor-pointer rounded-lg transition-all duration-300 relative group aspect-square p-0.5"
  111. :class="{
  112. 'bg-gradient-to-r from-blue-500 to-blue-600':
  113. currentImage === image,
  114. 'hover:bg-gradient-to-r hover:from-blue-500/50 hover:to-blue-600/50':
  115. currentImage !== image,
  116. 'opacity-50':
  117. isThumbnailLoading[index] || thumbnailErrors[index],
  118. }"
  119. >
  120. <!-- 缩略图加载状态 -->
  121. <div
  122. v-if="isThumbnailLoading[index]"
  123. class="absolute inset-0 flex items-center justify-center bg-zinc-800/50 rounded-lg z-10"
  124. >
  125. <div
  126. class="animate-spin h-4 w-4 border-2 border-blue-500 rounded-full border-t-transparent"
  127. ></div>
  128. </div>
  129. <!-- 缩略图遮罩 -->
  130. <div
  131. class="absolute inset-0 bg-black/0 transition-all duration-300 rounded-lg"
  132. :class="{
  133. 'bg-black/30': currentImage === image,
  134. 'group-hover:bg-black/20': currentImage !== image,
  135. }"
  136. ></div>
  137. <img
  138. :src="image"
  139. :alt="`${product.name} - 画像 ${index + 1}`"
  140. class="w-full h-full object-cover transition-all duration-300 rounded-lg"
  141. :class="{
  142. 'opacity-0': isThumbnailLoading[index],
  143. 'opacity-100': !isThumbnailLoading[index] && !thumbnailErrors[index],
  144. 'group-hover:scale-110': currentImage !== image,
  145. }"
  146. @load="handleThumbnailLoad(index)"
  147. @error="handleThumbnailError(index)"
  148. />
  149. <!-- 选中标记 -->
  150. <div
  151. v-if="currentImage === image"
  152. class="absolute top-1 right-1 w-4 h-4 bg-blue-500 rounded-full flex items-center justify-center"
  153. >
  154. <div class="w-2 h-2 bg-white rounded-full"></div>
  155. </div>
  156. <!-- 缩略图错误提示 -->
  157. <div
  158. v-if="thumbnailErrors[index]"
  159. class="absolute inset-0 flex items-center justify-center bg-red-900/50 rounded-lg"
  160. >
  161. <div class="flex flex-col items-center gap-1">
  162. <span class="text-white text-xs">エラー</span>
  163. <button
  164. @click.stop="retryLoadThumbnail(index)"
  165. class="px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600 transition-colors duration-300"
  166. >
  167. 再試行
  168. </button>
  169. </div>
  170. </div>
  171. </div>
  172. </div>
  173. </div>
  174. <!-- 右侧产品信息 -->
  175. <div class="flex flex-col gap-8">
  176. <!-- 产品名称 -->
  177. <div class="bg-zinc-900 rounded-lg p-6">
  178. <h1 class="text-white text-3xl font-medium mb-4">
  179. {{ product.title || product.name }}
  180. </h1>
  181. <div class="text-stone-400 text-lg leading-relaxed">
  182. {{ product.summary }}
  183. </div>
  184. </div>
  185. <!-- 产品参数 -->
  186. <div class="bg-zinc-900 rounded-lg p-6">
  187. <h2 class="text-white text-xl font-medium mb-6">製品仕様</h2>
  188. <div class="grid grid-cols-1 gap-4">
  189. <div
  190. class="flex justify-between items-center py-2 border-b border-zinc-800"
  191. >
  192. <span class="text-stone-400">カテゴリー</span>
  193. <span class="text-white font-medium">{{
  194. product.category
  195. }}</span>
  196. </div>
  197. <div
  198. class="flex justify-between items-center py-2 border-b border-zinc-800"
  199. >
  200. <span class="text-stone-400">用途</span>
  201. <span class="text-white font-medium">{{
  202. product.usage?.join(", ")
  203. }}</span>
  204. </div>
  205. <div class="flex justify-between items-center py-2">
  206. <span class="text-stone-400">容量</span>
  207. <span class="text-white font-medium">{{
  208. product.capacities?.join(" / ")
  209. }}</span>
  210. </div>
  211. </div>
  212. </div>
  213. <!-- 产品描述 -->
  214. <div class="bg-zinc-900 rounded-lg p-6">
  215. <h2 class="text-white text-xl font-medium mb-6">产品描述</h2>
  216. <div
  217. class="text-stone-400 leading-relaxed space-y-4 prose prose-invert max-w-none"
  218. >
  219. {{ product.description }}
  220. </div>
  221. </div>
  222. <div class="bg-zinc-900 rounded-lg p-6">
  223. <h2 class="text-white text-xl font-medium mb-6">详细描述</h2>
  224. <div
  225. class="text-stone-400 leading-relaxed space-y-4 prose prose-invert max-w-none"
  226. >
  227. <ContentRenderer :value="product.content" />
  228. </div>
  229. </div>
  230. <!-- 相关产品 -->
  231. <div
  232. v-if="relatedProducts.length > 0"
  233. class="bg-zinc-900 rounded-lg p-6"
  234. >
  235. <h2 class="text-white text-xl font-medium mb-6">
  236. {{
  237. product.meta?.series && product.meta.series.length > 0
  238. ? "同シリーズ製品"
  239. : "関連製品"
  240. }}
  241. </h2>
  242. <div
  243. class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"
  244. >
  245. <nuxt-link
  246. v-for="relatedProduct in relatedProducts"
  247. :key="relatedProduct.id"
  248. :to="`/products/${relatedProduct.id}`"
  249. class="group"
  250. >
  251. <div
  252. class="bg-zinc-800 rounded-lg p-4 transition-all duration-300 hover:bg-zinc-700"
  253. >
  254. <div
  255. class="aspect-square mb-4 overflow-hidden rounded-lg"
  256. >
  257. <img
  258. :src="relatedProduct.image"
  259. :alt="relatedProduct.title || relatedProduct.name"
  260. class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
  261. />
  262. </div>
  263. <h3
  264. class="text-white text-lg font-medium mb-2 line-clamp-2"
  265. >
  266. {{ relatedProduct.title || relatedProduct.name }}
  267. </h3>
  268. <p class="text-stone-400 text-sm line-clamp-2">
  269. {{ relatedProduct.summary }}
  270. </p>
  271. </div>
  272. </nuxt-link>
  273. </div>
  274. </div>
  275. </div>
  276. </div>
  277. </div>
  278. </div>
  279. </div>
  280. </ErrorBoundary>
  281. </div>
  282. </template>
  283. <script setup lang="ts">
  284. /**
  285. * 产品详情页面
  286. * 展示产品主图、参数和描述
  287. */
  288. import { useErrorHandler } from "~/composables/useErrorHandler";
  289. import { useRoute, useI18n, useAsyncData } from "#imports";
  290. import { queryCollection } from "#imports";
  291. import { ContentRenderer } from "#components";
  292. const { error, isLoading } = useErrorHandler();
  293. const route = useRoute();
  294. const { locale, t } = useI18n();
  295. const id = route.params.id as string;
  296. // 图片状态
  297. const currentImage = ref<string>("");
  298. const isImageLoading = ref(true);
  299. const isThumbnailLoading = ref<boolean[]>([]);
  300. const imageError = ref(false);
  301. const thumbnailErrors = ref<boolean[]>([]);
  302. const preloadImage = ref<string | null>(null);
  303. // 滚动跟随相关
  304. const scrollContainer = ref<HTMLElement | null>(null);
  305. const isSticky = ref(false);
  306. interface Product {
  307. id: string;
  308. name: string;
  309. usage: string[];
  310. capacities: string[];
  311. category: string;
  312. categoryId: string;
  313. description: string;
  314. summary: string;
  315. image: string;
  316. gallery: string[];
  317. body: string;
  318. content?: any;
  319. meta?: {
  320. series?: string[];
  321. name?: string;
  322. title?: string;
  323. image?: string;
  324. summary?: string;
  325. };
  326. title?: string;
  327. }
  328. /**
  329. * 使用queryCollection获取产品数据
  330. */
  331. const { data: productContent } = await useAsyncData(
  332. `product-${id}`,
  333. async () => {
  334. try {
  335. // 使用queryCollection从content目录获取数据
  336. const content = await queryCollection("content")
  337. .where("path", "LIKE", `/products/${locale.value}/${id}`)
  338. .first();
  339. return content;
  340. } catch (err) {
  341. console.error("Error fetching product content:", err);
  342. error.value = new Error(t("products.loadError"));
  343. return null;
  344. }
  345. }
  346. );
  347. /**
  348. * 获取分类信息
  349. */
  350. const { data: categoryContent } = await useAsyncData(
  351. `category-${productContent.value?.meta?.categoryId}`,
  352. async () => {
  353. if (!productContent.value?.meta?.categoryId) return null;
  354. try {
  355. const content = await queryCollection("content")
  356. .where(
  357. "path",
  358. "LIKE",
  359. `/categories/${locale.value}/${productContent.value.meta?.categoryId}`
  360. )
  361. .first();
  362. return content;
  363. } catch (err) {
  364. console.error("Error fetching category:", err);
  365. return null;
  366. }
  367. },
  368. {
  369. immediate: !!productContent.value?.meta?.categoryId,
  370. }
  371. );
  372. /**
  373. * 使用计算属性解析产品数据
  374. */
  375. const product = computed<Product | null>(() => {
  376. if (!productContent.value) return null;
  377. // 提取产品数据
  378. const meta = productContent.value.meta || {};
  379. return {
  380. id: id,
  381. name: String(meta.name || productContent.value.title || ""),
  382. title: String(productContent.value.title || meta.name || ""),
  383. usage: Array.isArray(meta.usage) ? meta.usage : [],
  384. capacities: Array.isArray(meta.capacities) ? meta.capacities : [],
  385. category: categoryContent.value?.title || "",
  386. categoryId: meta.categoryId || "",
  387. description: productContent.value.description || "",
  388. summary: String(meta.summary || ""),
  389. image: String(meta.image || ""),
  390. gallery: Array.isArray(meta.gallery) ? meta.gallery : [],
  391. body: productContent.value.body || "",
  392. content: productContent.value,
  393. meta: {
  394. series: Array.isArray(meta.series) ? meta.series : [],
  395. name: String(meta.name || ""),
  396. title: String(productContent.value.title || ""),
  397. image: String(meta.image || ""),
  398. summary: String(meta.summary || ""),
  399. },
  400. };
  401. });
  402. /**
  403. * 获取相关产品
  404. */
  405. const { data: relatedProductsContent } = await useAsyncData(
  406. `related-products-${id}`,
  407. async () => {
  408. try {
  409. // 获取产品列表
  410. const content = await queryCollection("content")
  411. .where("path", "LIKE", `/products/${locale.value}/%`)
  412. .all();
  413. return content;
  414. } catch (err) {
  415. console.error("Error fetching related products:", err);
  416. return [];
  417. }
  418. }
  419. );
  420. /**
  421. * 处理相关产品数据
  422. */
  423. const relatedProducts = computed(() => {
  424. if (!relatedProductsContent.value || !product.value) return [];
  425. return relatedProductsContent.value
  426. .filter((item: any) => item._path !== `/products/${locale.value}/${id}`)
  427. .map((item: any) => {
  428. const meta = item.meta || {};
  429. console.log(meta);
  430. return {
  431. id: meta.name || "",
  432. name: meta.name || item.title || "",
  433. title: item.title || meta.name || "",
  434. image: meta.image || "",
  435. summary: meta.summary || "",
  436. };
  437. })
  438. .slice(0, 6); // 最多显示6个相关产品
  439. });
  440. /**
  441. * 预加载下一张图片
  442. */
  443. function preloadNextImage(image: string) {
  444. preloadImage.value = image;
  445. }
  446. /**
  447. * 处理预加载完成
  448. */
  449. function handlePreloadComplete() {
  450. preloadImage.value = null;
  451. }
  452. /**
  453. * 处理图片加载完成
  454. */
  455. function handleImageLoad() {
  456. isImageLoading.value = false;
  457. imageError.value = false;
  458. }
  459. /**
  460. * 处理图片加载错误
  461. */
  462. function handleImageError() {
  463. isImageLoading.value = false;
  464. imageError.value = true;
  465. }
  466. /**
  467. * 重试加载图片
  468. */
  469. function retryLoadImage() {
  470. isImageLoading.value = true;
  471. imageError.value = false;
  472. // 强制重新加载图片
  473. const img = new Image();
  474. img.src = currentImage.value;
  475. img.onload = () => {
  476. handleImageLoad();
  477. };
  478. img.onerror = () => {
  479. handleImageError();
  480. };
  481. }
  482. /**
  483. * 重试加载缩略图
  484. */
  485. function retryLoadThumbnail(index: number) {
  486. // 确保index在有效范围内
  487. if (index < 0) {
  488. console.error('Invalid thumbnail index:', index);
  489. return;
  490. }
  491. const images = [product.value?.image, ...(product.value?.gallery || [])];
  492. const imageUrl = images[index];
  493. // 检查图片URL是否有效
  494. if (!imageUrl) {
  495. console.error('Invalid image URL for thumbnail:', index);
  496. thumbnailErrors.value[index] = true;
  497. isThumbnailLoading.value[index] = false;
  498. return;
  499. }
  500. console.log('Retrying thumbnail load:', { index, imageUrl });
  501. isThumbnailLoading.value[index] = true;
  502. thumbnailErrors.value[index] = false;
  503. // 创建新的图片对象并设置超时
  504. const img = new Image();
  505. const timeoutId = setTimeout(() => {
  506. console.error('Thumbnail load timeout:', index);
  507. handleThumbnailError(index);
  508. }, 10000); // 10秒超时
  509. img.onload = () => {
  510. clearTimeout(timeoutId);
  511. console.log('Thumbnail loaded successfully:', index);
  512. handleThumbnailLoad(index);
  513. };
  514. img.onerror = (error) => {
  515. clearTimeout(timeoutId);
  516. console.error('Thumbnail load error:', { index, error });
  517. handleThumbnailError(index);
  518. };
  519. // 设置跨域属性
  520. img.crossOrigin = 'anonymous';
  521. // 最后设置src以开始加载
  522. img.src = imageUrl;
  523. }
  524. /**
  525. * 处理缩略图加载完成
  526. */
  527. function handleThumbnailLoad(index: number) {
  528. console.log('Thumbnail load handler called:', index);
  529. // 确保数组索引存在
  530. if (typeof isThumbnailLoading.value[index] === 'undefined') {
  531. console.warn('Thumbnail index out of bounds:', index);
  532. return;
  533. }
  534. // 直接修改对应索引的状态
  535. isThumbnailLoading.value[index] = false;
  536. thumbnailErrors.value[index] = false;
  537. }
  538. /**
  539. * 处理缩略图加载错误
  540. */
  541. function handleThumbnailError(index: number) {
  542. console.log('Thumbnail error handler called:', index);
  543. // 确保数组索引存在
  544. if (typeof isThumbnailLoading.value[index] === 'undefined') {
  545. console.warn('Thumbnail index out of bounds:', index);
  546. return;
  547. }
  548. // 直接修改对应索引的状态
  549. isThumbnailLoading.value[index] = false;
  550. thumbnailErrors.value[index] = true;
  551. }
  552. /**
  553. * 切换图片
  554. */
  555. function changeImage(image: string | undefined) {
  556. if (image && image !== currentImage.value) {
  557. isImageLoading.value = true;
  558. imageError.value = false;
  559. preloadNextImage(image);
  560. currentImage.value = image;
  561. }
  562. }
  563. // 页面加载时初始化状态
  564. onMounted(() => {
  565. // 设置当前图片
  566. if (product.value?.image) {
  567. currentImage.value = product.value.image;
  568. }
  569. // 初始化缩略图加载状态数组
  570. const galleryLength = (product.value?.gallery?.length || 0) + 1;
  571. isThumbnailLoading.value = new Array(galleryLength).fill(true);
  572. thumbnailErrors.value = new Array(galleryLength).fill(false);
  573. // 预加载所有缩略图
  574. const images = [product.value?.image, ...(product.value?.gallery || [])];
  575. images.forEach((image, index) => {
  576. if (image) {
  577. const img = new Image();
  578. img.onload = () => handleThumbnailLoad(index);
  579. img.onerror = () => handleThumbnailError(index);
  580. img.src = image;
  581. }
  582. });
  583. console.log('Initialized thumbnail states:', {
  584. loading: isThumbnailLoading.value,
  585. errors: thumbnailErrors.value
  586. });
  587. // 添加滚动监听
  588. scrollContainer.value = document.querySelector('.max-w-screen-2xl');
  589. if (scrollContainer.value) {
  590. window.addEventListener('scroll', handleScroll, { passive: true });
  591. }
  592. });
  593. // 清理滚动监听
  594. onUnmounted(() => {
  595. if (scrollContainer.value) {
  596. window.removeEventListener('scroll', handleScroll);
  597. }
  598. });
  599. // 处理滚动事件
  600. function handleScroll() {
  601. if (!scrollContainer.value) return;
  602. const containerRect = scrollContainer.value.getBoundingClientRect();
  603. const scrollTop = window.scrollY || document.documentElement.scrollTop;
  604. // 当容器顶部距离视窗顶部小于100px时,启用sticky
  605. isSticky.value = containerRect.top < 100;
  606. }
  607. // SEO优化
  608. useHead(() => ({
  609. title: `${product.value?.name || "产品详情"} - Hanye`,
  610. meta: [
  611. {
  612. name: "description",
  613. content: product.value?.description || "产品详情页面",
  614. },
  615. ],
  616. }));
  617. </script>
  618. <style lang="scss" scoped>
  619. /* 隐藏滚动条但保持滚动功能 */
  620. .scrollbar-hide {
  621. -ms-overflow-style: none; /* IE and Edge */
  622. scrollbar-width: none; /* Firefox */
  623. }
  624. .scrollbar-hide::-webkit-scrollbar {
  625. display: none; /* Chrome, Safari and Opera */
  626. }
  627. /* 图片过渡动画 */
  628. .main-image {
  629. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  630. }
  631. /* 缩略图悬停效果 */
  632. .thumbnail-item {
  633. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  634. }
  635. .thumbnail-item:hover {
  636. transform: translateY(-2px);
  637. }
  638. /* 缩略图选中效果 */
  639. .thumbnail-item.selected {
  640. transform: scale(1.05);
  641. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
  642. 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  643. }
  644. /* 产品信息卡片效果 */
  645. .info-card {
  646. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  647. }
  648. .info-card:hover {
  649. transform: translateY(-2px);
  650. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
  651. 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  652. }
  653. /* 滚动跟随效果 */
  654. .lg\:sticky {
  655. position: sticky;
  656. top: 6rem; /* 96px */
  657. transition: all 0.3s ease;
  658. z-index: 10;
  659. max-height: calc(100vh - 6rem);
  660. overflow-y: auto;
  661. }
  662. @media (max-width: 1024px) {
  663. .lg\:sticky {
  664. position: relative;
  665. top: 0;
  666. max-height: none;
  667. }
  668. }
  669. </style>